﻿// <copyright file="Telegram.cs" company="Hallvard Korsgaard">
//  Microsoft Public License (Ms-PL). See license details here: http://enoceannet.codeplex.com/license
// </copyright>
// <author>Hallvard Korsgaard</author>

namespace EnOcean
{
  using System;
  using System.Globalization;
  using System.Text;
  using EnOcean.Command;
  using EnOcean.Message;
  using EnOcean.Message.Error;
  using EnOcean.Radio;
  using EnOcean.Radio.Modem;
  using EnOcean.Radio.Sensor;

  /// <summary>
  /// This is the base class for all telegram types. It contatins common properties
  /// and methods for parsing and serializing telegrams to the format required by
  /// the TCM 120 module.
  /// </summary>
  [Serializable]
  public abstract class Telegram
  {
    /// <summary>
    /// Gets or sets the type of telegram. This is the H_SEQ field in the TCM 120 message.
    /// </summary>
    public TelegramType TelegramType
    {
      get;
      set;
    }

    /// <summary>
    /// Parses the ASCII representation of a message in byte format (as returned from the ToString() method).
    /// </summary>
    /// <param name="message">The ASCII representation of a message in byte format.</param>
    /// <returns>The object representation of the message.</returns>
    public static Telegram Parse(string message)
    {
      byte[] m;

      if (string.IsNullOrEmpty(message))
      {
        throw new ArgumentNullException("message");
      }

      if (message.Length != 28)
      {
        throw new ArgumentOutOfRangeException("message", "Message should be 28 characters.");
      }

      m = new byte[14];

      for (int i = 0; i < 14; i++)
      {
        m[i] = byte.Parse(message.Substring(2 * i, 2), NumberStyles.HexNumber, NumberFormatInfo.InvariantInfo);
      }

      return Parse(m);
    }

    /// <summary>
    /// Parses a message in byte format.
    /// </summary>
    /// <param name="message">A message in byte format (14 bytes).</param>
    /// <returns>The object representation of the message.</returns>
    public static Telegram Parse(byte[] message)
    {
      byte checksum;
      TelegramType telegramType;

      if (message == null)
      {
        throw new ArgumentNullException("message");
      }

      if (message.Length != 14)
      {
        throw new ArgumentOutOfRangeException("message", "Message should be 14 bytes.");
      }

      if (message[0] != 0xA5 || message[1] != 0x5A)
      {
        throw new ArgumentException("Invalid sync bytes.");
      }

      // Checksum
      checksum = 0;
      for (int i = 2; i < 13; i++)
      {
        checksum += message[i];
      }

      if (checksum != message[13])
      {
        throw new ArgumentException("Checksum error.");
      }

      telegramType = ParseType(message[2]);

      if (telegramType == TelegramType.ReceiveRadioTelegram || telegramType == TelegramType.TransmitRadioTelegram)
      {
        return ParseRadioTelegram(message);
      }
      else if (telegramType == TelegramType.ReceiveMessageTelegram)
      {
        return ParseMessageTelegram(message);
      }
      else if (telegramType == TelegramType.TransmitCommandTelegram)
      {
        return ParseCommandTelegram(message);
      }
      else
      {
        throw new ArgumentOutOfRangeException("Unknown telegram type");
      }
    }

    /// <summary>
    /// Converts the sensor value to a real world value.
    /// </summary>
    /// <param name="fromValue">The value read from the sensor.</param>
    /// <param name="fromRangeStart">The lowest possible sensor value. Use the value from the sensors data sheet.</param>
    /// <param name="fromRangeEnd">The highest possible sensor value. Use the value from the sensors data sheet.</param>
    /// <param name="toRangeStart">The lowest possible real world value. Use the value from the sensors data sheet.</param>
    /// <param name="toRangeEnd">The highest possible real world value. Use the value from the sensors data sheet.</param>
    /// <returns>The real world value. E.g. the temperature in celcius for a temperature sensor.</returns>
    public static decimal ConvertRange(byte fromValue, byte fromRangeStart, byte fromRangeEnd, decimal toRangeStart, decimal toRangeEnd)
    {
      decimal fromRange;
      decimal toRange;
      decimal toValue;

      fromRange = (decimal)fromRangeEnd - (decimal)fromRangeStart;
      toRange = toRangeEnd - toRangeStart;

      toValue = ((((decimal)fromValue - (decimal)fromRangeStart) * toRange) / fromRange) + toRangeStart;

      return toValue;
    }

    /// <summary>
    /// Creates the byte representation of the telegram.
    /// </summary>
    /// <returns>A byte array of length 14 for all telegrams, except the Wake telegram that has a length of 1.</returns>
    public byte[] GetBytes()
    {
      byte[] message;

      if (this is Wake)
      {
        message = new byte[1];
      }
      else
      {
        message = new byte[14];
      }

      this.GetBytes(message);

      return message;
    }

    /// <summary>
    /// Creates the byte representation of the telegram.
    /// </summary>
    /// <param name="message">A byte array of length 14 for all telegrams, except the Wake telegram that has a length of 1.</param>
    public void GetBytes(byte[] message)
    {
      this.CreateMessage(message);

      if (message.Length >= 14)
      {
        // Checksum
        message[13] = 0;
        for (int i = 2; i < 13; i++)
        {
          message[13] += message[i];
        }
      }
    }

    /// <summary>
    /// Returns the ASCII representation of the message in byte format.
    /// </summary>
    /// <returns>The ASCII representation of the message in byte format.</returns>
    public override string ToString()
    {
      byte[] message;

      if (this is Wake)
      {
        message = new byte[2];
      }
      else
      {
        message = new byte[28];
      }

      this.GetBytes(message);

      for (int i = (message.Length / 2) - 1; i >= 0; i--)
      {
        message[(2 * i) + 1] = ConvertToAsciiChar((byte)(message[i] & 0xf));
        message[(2 * i)] = ConvertToAsciiChar((byte)(message[i] >> 4));
      }

      return ASCIIEncoding.ASCII.GetString(message);
    }

    /// <summary>
    /// Creates the byte format of the message.
    /// </summary>
    /// <param name="message">A byte array of length 14 to hold the message contents.</param>
    protected internal virtual void CreateMessage(byte[] message)
    {
      if (message == null)
      {
        throw new ArgumentNullException("message");
      }

      if (message.Length < 14)
      {
        throw new ArgumentOutOfRangeException("message", "array length must be at least 14 to hold the message content.");
      }

      message[0] = 0xA5; // Sync
      message[1] = 0x5A; // Sync
      message[2] = (byte)((((byte)TelegramType) << 5) | 0xB);
    }

    /// <summary>
    /// Converts a 4 bit number to the ASCII representation.
    /// </summary>
    /// <param name="value">A value in the  range 0x0 to 0xF inclusive.</param>
    /// <returns>The ASCII value representing the number.</returns>
    private static byte ConvertToAsciiChar(byte value)
    {
      if (value >= 0x0 && value <= 0x9)
      {
        return (byte)(value + 0x30);
      }
      else if (value >= 0xA && value <= 0XF)
      {
        return (byte)(value + 0x37);
      }
      else
      {
        throw new ArgumentOutOfRangeException("value", "must be in the range 0x0 to 0xF inclusive");
      }
    }

    /// <summary>
    /// Parses a message telegram in byte format.
    /// </summary>
    /// <param name="message">A message telegram in byte format (14 bytes).</param>
    /// <returns>The object representation of the message telegram.</returns>
    private static MessageTelegram ParseMessageTelegram(byte[] message)
    {
      switch (message[3])
      {
        case (byte)MessageType.Ok:
          return new Ok();

        case (byte)MessageType.InformIdBase:
          return new InformIdBase()
          {
            IdBase = ParseId(message[4], message[5], message[6], message[7])
          };

        case (byte)MessageType.InformModemStatus:
          return new InformModemStatus()
          {
            IsModemOn = message[4] == 1,
            ModemId = ParseId(message[5], message[6])
          };

        case (byte)MessageType.InformRadioSensitivity:
          return new InformRadioSensitivity()
          {
            Sensitivity = (RadioSensitivity)message[4]
          };

        case (byte)MessageType.InformSoftwareVersion:
          return new InformSoftwareVersion()
          {
            VersionPosition1 = message[4],
            VersionPosition2 = message[5],
            VersionPosition3 = message[6],
            VersionPosition4 = message[7]
          };

        case (byte)MessageType.InformInit:
          return new InformInit()
          {
            Text = Encoding.ASCII.GetString(message, 4, 9)
          };

        case (byte)MessageType.Error:
          return new Error();

        case (byte)MessageType.AcknowledgeNotWantedError:
          return new AcknowledgeNotWantedError();

        case (byte)MessageType.DuplicateModemIdError:
          return new DuplicateModemIdError();

        case (byte)MessageType.IdOutOfRangeError:
          return new IdBaseOutOfRangeError();

        case (byte)MessageType.NoAcknowledgeError:
          return new NoAcknowledgeError();

        case (byte)MessageType.TelegramIdOutOfRangeError:
          return new TelegramIdOutOfRangeError();

        case (byte)MessageType.HseqError:
          return new HseqError();

        case (byte)MessageType.LengthError:
          return new LengthError();

        case (byte)MessageType.OrgError:
          return new OrgError();

        case (byte)MessageType.ChecksumError:
          return new ChecksumError();

        default:
          throw new ArgumentOutOfRangeException("Unknown message telegram.");
      }
    }

    /// <summary>
    /// Parses a command telegram in byte format.
    /// </summary>
    /// <param name="message">A command telegram in byte format (14 bytes).</param>
    /// <returns>The object representation of the command telegram.</returns>
    private static CommandTelegram ParseCommandTelegram(byte[] message)
    {
      if (message[3] == (byte)CommandType.SetIdBase)
      {
        return new SetIdBase(ParseId(message[4], message[5], message[6], message[7]));
      }
      else if (message[3] == (byte)CommandType.ReadIdBase)
      {
        return new ReadIdBase();
      }
      else if (message[3] == (byte)CommandType.SetRadioSensitivity)
      {
        return new SetRadioSensitivity((RadioSensitivity)message[4]);
      }
      else if (message[3] == (byte)CommandType.ReadRadioSensitivity)
      {
        return new ReadRadioSensitivity();
      }
      else if (message[3] == (byte)CommandType.Sleep)
      {
        return new Sleep();
      }
      else if (message[3] == (byte)CommandType.Wake)
      {
        return new Wake();
      }
      else if (message[3] == (byte)CommandType.Reset)
      {
        return new Reset();
      }
      else if (message[3] == (byte)CommandType.ModemOn)
      {
        return new ModemOn(ParseId(message[4], message[5]));
      }
      else if (message[3] == (byte)CommandType.ModemOff)
      {
        return new ModemOff();
      }
      else if (message[3] == (byte)CommandType.ReadModemStatus)
      {
        return new ReadModemStatus();
      }
      else if (message[3] == (byte)CommandType.ReadSoftwareVersion)
      {
        return new ReadSoftwareVersion();
      }
      else
      {
        throw new ArgumentOutOfRangeException("Unknown command telegram.");
      }
    }

    /// <summary>
    /// Parses a radio telegram in byte format.
    /// </summary>
    /// <param name="message">A radio telegram in byte format (14 bytes).</param>
    /// <returns>The object representation of the radio telegram.</returns>
    private static RadioTelegram ParseRadioTelegram(byte[] message)
    {
      if (message[3] == (byte)Org.RepeatedSwitch && (message[12] & 0x10) == 0x10)
      {
        return ParseRepeatedSwitchNTelegram(message);
      }
      else if (message[3] == (byte)Org.RepeatedSwitch && (message[12] & 0x10) == 0x00)
      {
        return ParseRepeatedSwitchUTelegram(message);
      }
      else if (message[3] == (byte)Org.OneByteSensor)
      {
        return ParseOneByteSensorTelegram(message);
      }
      else if (message[3] == (byte)Org.FourByteSensor && (message[7] & 0x80) == 0)
      {
        return ParseFourByteSensorDataTelegram(message);
      }
      else if (message[3] == (byte)Org.FourByteSensor && (message[7] & 0x80) == 0x80)
      {
        return ParseFourByteSensorTeachTelegram(message);
      }
      else if (message[3] == (byte)Org.Modem)
      {
        return ParseModemTelegram(message);
      }
      else if (message[3] == (byte)Org.ModemAcknowledge)
      {
        return ParseModemAcknowledgeTelegram(message);
      }
      else
      {
        throw new ArgumentException("Unknown message (ORG) type");
      }
    }

    /// <summary>
    /// Parses a RPS N telegram.
    /// </summary>
    /// <param name="message">The message to parse.</param>
    /// <returns>A RepeatedSwitchNTelegram object.</returns>
    private static RepeatedSwitchNTelegram ParseRepeatedSwitchNTelegram(byte[] message)
    {
      RepeatedSwitchNTelegram t;
      byte id;

      // RepeatedSwitchNTelegram
      t = new RepeatedSwitchNTelegram();
      ParseSensorTelegramCommon(message, t);

      if ((message[12] & 0x20) == 0x00)
      {
        t.SwitchType = SwitchType.Type1;
      }
      else
      {
        t.SwitchType = SwitchType.Type2;
      }

      id = (byte)(message[4] & 0xC0);
      t.FirstButtonId = (byte)(id >> 6);

      if ((message[4] & 0x20) == 0x20)
      {
        t.FirstButtonState = ButtonState.O;
      }
      else
      {
        t.FirstButtonState = ButtonState.I;
      }

      t.IsEnergyBowPressed = (message[4] & 0x10) == 0x10;

      id = (byte)(message[4] & 0x0C);
      t.SecondButtonId = (byte)(id >> 2);

      if ((message[4] & 0x02) == 0x02)
      {
        t.SecondButtonState = ButtonState.O;
      }
      else
      {
        t.SecondButtonState = ButtonState.I;
      }

      t.HasSecondAction = (message[4] & 0x01) == 0x01;

      return t;
    }

    /// <summary>
    /// Parses a RPS U telegram.
    /// </summary>
    /// <param name="message">The message to parse.</param>
    /// <returns>A RepeatedSwitchUTelegram object.</returns>
    private static RepeatedSwitchUTelegram ParseRepeatedSwitchUTelegram(byte[] message)
    {
      RepeatedSwitchUTelegram t;

      t = new RepeatedSwitchUTelegram();
      ParseSensorTelegramCommon(message, t);

      if ((message[12] & 0x20) == 0x00)
      {
        t.SwitchType = SwitchType.Type1;
      }
      else
      {
        t.SwitchType = SwitchType.Type2;
      }

      t.Buttons = (byte)(message[4] & 0xE0);
      t.Buttons >>= 5;
      t.IsEnergyBowPressed = (message[4] & 0x10) == 0x10;

      return t;
    }

    /// <summary>
    /// Parses a 1BS telegram.
    /// </summary>
    /// <param name="message">The message to parse.</param>
    /// <returns>A OneByteSensorTelegram object.</returns>
    private static OneByteSensorTelegram ParseOneByteSensorTelegram(byte[] message)
    {
      OneByteSensorTelegram t;

      t = new OneByteSensorTelegram()
      {
        Data = message[4]
      };

      ParseSensorTelegramCommon(message, t);

      return t;
    }

    /// <summary>
    /// Parses a 4BS data telegram.
    /// </summary>
    /// <param name="message">The message to parse.</param>
    /// <returns>A FourByteSensorDataTelegram object.</returns>
    private static FourByteSensorDataTelegram ParseFourByteSensorDataTelegram(byte[] message)
    {
      FourByteSensorDataTelegram t;

      t = new FourByteSensorDataTelegram()
      {
        Data3 = message[4],
        Data2 = message[5],
        Data1 = message[6],
        Data0 = message[7]
      };

      ParseSensorTelegramCommon(message, t);

      return t;
    }

    /// <summary>
    /// Parses a 4BS teach telegram.
    /// </summary>
    /// <param name="message">The message to parse.</param>
    /// <returns>A FourByteSensorTeachTelegram object.</returns>
    private static FourByteSensorTeachTelegram ParseFourByteSensorTeachTelegram(byte[] message)
    {
      FourByteSensorTeachTelegram t;

      byte b1 = message[4];
      byte b2 = (byte)(message[4] >> 2);
      
      t = new FourByteSensorTeachTelegram()
      {
        Function = (FourByteSensorFunction)((byte)(message[4] >> 2)),
        DeviceType = (byte)((message[5] >> 3) | ((message[4] & 0x3) << 5)),
        Manufacturer = (Manufacturer)(message[6] | ((message[5] & 0x7) << 8))
      };

      ParseSensorTelegramCommon(message, t);

      return t;
    }

    /// <summary>
    /// Parses a 6DT telegram.
    /// </summary>
    /// <param name="message">The message to parse.</param>
    /// <returns>A ModemTelegram object.</returns>
    private static ModemTelegram ParseModemTelegram(byte[] message)
    {
      return new ModemTelegram()
      {
        TelegramType = ParseType(message[2]),
        Data5 = message[4],
        Data4 = message[5],
        Data3 = message[6],
        Data2 = message[7],
        Data1 = message[8],
        Data0 = message[9],
        RecipientId = ParseId(message[10], message[11]),
        RepeatCounter = (byte)(message[12] & 0x0F)
      };
    }

    /// <summary>
    /// Parses a MDA telegram.
    /// </summary>
    /// <param name="message">The message to parse.</param>
    /// <returns>A ModemAcknowledgeTelegram object.</returns>
    private static ModemAcknowledgeTelegram ParseModemAcknowledgeTelegram(byte[] message)
    {
      return new ModemAcknowledgeTelegram()
      {
        TelegramType = ParseType(message[2]),
        AnsweringId = ParseId(message[8], message[9]),
        RepeatCounter = (byte)(message[12] & 0x0F)
      };
    }

    /// <summary>
    /// Parses common properties of sensor telegrams.
    /// </summary>
    /// <param name="message">A radio telegram in byte format (14 bytes).</param>
    /// <param name="telegram">A SensorTelegram whos properties should be set.</param>
    private static void ParseSensorTelegramCommon(byte[] message, SensorTelegram telegram)
    {
      telegram.TelegramType = ParseType(message[2]);
      telegram.RepeatCounter = (byte)(message[12] & 0x0F);
      telegram.Id = ParseId(message[8], message[9], message[10], message[11]);
    }

    /// <summary>
    /// Checks the H_SEQ and Length byte of the message.
    /// </summary>
    /// <param name="type">The H_SEQ and Length byte of the message.</param>
    /// <returns>A TelegramType corresponding to the H_SEQ field.</returns>
    private static TelegramType ParseType(byte type)
    {
      byte t;

      if ((type & 0x1F) != 0xB)
      {
        throw new ArgumentOutOfRangeException("Unsupported message length. Length must be 11.");
      }

      t = (byte)(type >> 5);

      if (t == 0)
      {
        return TelegramType.ReceiveRadioTelegram;
      }
      else if (t == 3)
      {
        return TelegramType.TransmitRadioTelegram;
      }
      else if (t == 4)
      {
        return TelegramType.ReceiveMessageTelegram;
      }
      else if (t == 5)
      {
        return TelegramType.TransmitCommandTelegram;
      }
      else
      {
        throw new ArgumentOutOfRangeException("Unknown telegram type.");
      }
    }

    /// <summary>
    /// Creates the message ID from the four individual bytes.
    /// </summary>
    /// <param name="b3">Byte 3 of the ID. This is the MSB byte.</param>
    /// <param name="b2">Byte 2 of the ID.</param>
    /// <param name="b1">Byte 1 of the ID.</param>
    /// <param name="b0">Byte 0 of the ID. This is the LSB byte.</param>
    /// <returns>The message ID.</returns>
    private static long ParseId(byte b3, byte b2, byte b1, byte b0)
    {
      long id;

      id = b3;
      id <<= 8;

      id |= b2;
      id <<= 8;

      id |= b1;
      id <<= 8;

      id |= b0;

      return id;
    }

    /// <summary>
    /// Creates the modem ID from the two individual bytes.
    /// </summary>
    /// <param name="b1">Byte 1 of the ID. This is the MSB byte.</param>
    /// <param name="b0">Byte 0 of the ID. This is the LSB byte</param>
    /// <returns>The modem ID.</returns>
    private static int ParseId(byte b1, byte b0)
    {
      int id;

      id = b1;
      id <<= 8;

      id |= b0;

      return id;
    }
  }
}
